/**
 * 
 */
package three_star_craft.uap;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.util.HashMap;

import org.grlea.log.SimpleLogger;

import three_star_craft.EventData;
import three_star_craft.Generator;
import three_star_craft.uap.event.EventUapConnectionCfm;
import three_star_craft.uap.event.EventUapConnectionRej;
import three_star_craft.uap.event.EventUapConnectionReleaseCfm;
import three_star_craft.uap.event.EventUapMediaBearerSetupCfm;
import three_star_craft.uap.state.UapStateBase;
import three_star_craft.uap.state.UapStateMap;
import three_star_craft.uap.timer.Timer;
import three_star_craft.uap.timer.TimerEvent;
import UserApplicationPart.UAP;

/**
 * The generator of UAP protocol in user_client state machine
 * 
 * @author w.antosiewic
 */
public class UapGenerator extends Generator
{
    private static final SimpleLogger logger = new SimpleLogger(UapGenerator.class);

    private int _clientId;
    
    private UapConnectionListener _connectionListener;

    private UapStateBase _currentState;

    private Socket _mediaSocket;
    private int _audioCapabilities;
    private int _videoCapabilities;

    private StringBuilder _debugString;

    private Timer _tWaitForUapConnCfm;
    private Timer _tWaitForMediaBearerCfm;

    /**
     * Value which will be assigned as generator id for the next created generator
     * 8 bit value
     */
    private static int nextGeneratorId = 1;

    private static HashMap<Integer, UapGenerator> _generatorMap = new HashMap<Integer, UapGenerator>();

    /**
     * Searches generator object based on the generator ID
     * @param connectionId ID of generator to find
     * @return UapGenerator instance with the specified ID or <code>null</code>
     * if the generator is not found
     */
    public static UapGenerator findGenerator(int connectionId)
    {
        return _generatorMap.get(connectionId);
    }

    /**
     * 
     */
    public UapGenerator(UapConnectionListener connectionListener, int clientId)
    {
        _connectionListener = connectionListener;
        _currentState = UapStateMap.instance().startState();

        _audioCapabilities = UAP.AudioCodec.AUDIO_C_WAV_VALUE;
        _videoCapabilities = UAP.VideoCodec.VIDEO_UNAVAILABLE_VALUE;

        _tWaitForUapConnCfm = new Timer(this,
                UapSigNo.UAP_TIMER_WAIT_CONNECTION_CFM,
                UapConstants.WAIT_FOR_CONNECT_CFM_TIMEOUT);

        _tWaitForMediaBearerCfm = new Timer(this,
                UapSigNo.UAP_TIMER_WAIT_MEDIA_BEARER_CFM,
                UapConstants.WAIT_FOR_MEDIA_BEARER_CFM_TIMEOUT);

        _generatorId = nextGeneratorId;
        _clientId = clientId;
        if(++nextGeneratorId > 0xFF)
        {
            nextGeneratorId = 0;
        }
        int connectionId = _clientId << 8 | _generatorId; 
        _generatorMap.put(connectionId, this);

        logger.debugObject("Created new UapGenerator with id=0x", Integer.toHexString(_generatorId));
        logger.debugObject("Added generator mapping for connectionId=0x", Integer.toHexString(connectionId));
    }

    /**
     * @return the _currentState
     */
    public UapStateBase getCurrentState()
    {
        return _currentState;
    }

    /**
     * Changes state of this generator. If new and current state are the
     * same this method has no effect
     * @param newState
     */
    public void changeState(UapStateBase newState) throws NullPointerException
    {
        if(newState == null)
        {
            throw new NullPointerException("newState param is null");
        }
        // all states are singletons so we can compare references
        if(_currentState != newState)
        {
            logger.debug("Changing state from: "+_currentState+" to "+newState);
            _currentState = newState;
        }
    }

    /**
     * Gets connectionID combined from clientID and this generator ID
     * @return ID of connection associated with this generator
     */
    public int getConnectionId()
    {
        return _clientId << 8 | _generatorId;
    }
    
    public void handleEvent(EventData event)
    {
        logger.debugObject("handleEvent for: ", event);

        switch(event.sigNo())
        {
        case UAP_CONNECTION_CFM:
            _currentState.handleUapConnectionConfirm(this,
                    (EventUapConnectionCfm)event);
            break;

        case UAP_CONNECTION_REJ:
            _currentState.handleUapConnectionReject(this,
                    (EventUapConnectionRej)event);
            break;

        case UAP_TIMER_WAIT_CONNECTION_CFM:
            _currentState.handleUapTimerWaitForConnectionCfm(this,
                    (TimerEvent)event);
            break;

        case UAP_CONNECTION_RELEASE_CFM:
            _currentState.handleUapConnectionReleaseConfirm(this,
                    (EventUapConnectionReleaseCfm)event);
            break;

        case UAP_TIMER_WAIT_MEDIA_BEARER_CFM:
            _currentState.handleUapTimerWaitForMediaBearerCfm(this,
                    (TimerEvent)event);

        case UAP_MEDIA_BEARER_SETUP_CFM:
            _currentState.handleUapMediaBearerSetupConfirm(this,
                    (EventUapMediaBearerSetupCfm)event);
            
        default:
            break;
        }
    }



    public String getDebugInfo()
    {
        if(_debugString == null)
        {
            _debugString = new StringBuilder("UapGenerator:0x").append(Integer.toHexString(_generatorId));
            _debugString.append(",audioCap:0x").append(Integer.toHexString(_audioCapabilities));
            _debugString.append(",videoCap:").append(Integer.toHexString(_videoCapabilities));
        }
        return _debugString.toString();
    }

    public boolean isConnected()
    {
        return _currentState != UapStateMap.UAP_IDLE;
    }


    public int getVideoCapabilities()
    {
        return _videoCapabilities;
    }

    public int getAudioCapabilities()
    {
        return _audioCapabilities;
    }

    public void startTWaitForConnCfm()
    {
        _tWaitForUapConnCfm.start();
    }

    public void cancelTWaitForConnCfm()
    {
        _tWaitForUapConnCfm.cancel();
    }

    public void startTWaitForMediaBearerCfm()
    {
        _tWaitForMediaBearerCfm.start();
    }
    
    public void cancelTWaitForMediaBearerCfm()
    {
        _tWaitForMediaBearerCfm.cancel();
    }
    
    /**
     * @return the _connectionListener
     */
    public UapConnectionListener getUapConnectionListener()
    {
        return _connectionListener;
    }

    public boolean initiateMediaSocket()
    {
        // create socket which is not connected to external address - yet
        _mediaSocket = new Socket();
        // bind socket to local address and port - let the port be chosen
        try
        {
            _mediaSocket.bind(null);
        } catch (IOException e)
        {
            logger.errorException(e);
            return false;
        }
        return true;
    }
    
    public void closeMediaSocket()
    {
        if(_mediaSocket != null)
        {
            try
            {
                _mediaSocket.close();
            } catch (IOException e)
            {
                logger.errorException(e);
            }
        }
    }
    
    public int getMediaPort()
    {
        if(_mediaSocket == null)
        {
            logger.error("Trying to retrieve media socket's port but socket "
                    + "was not initialized");
            return -1;
        }
        return _mediaSocket.getLocalPort();
    }

    /**
     * Connects previously initialized media socket to the specified
     * address and port.
     * 
     * @param serverMediaAddress IP address of Media Gateway server
     * @param serverMediaPort port on which the media socket is to be opened
     * @return <code>true</code> on successful connection, <code>false</code> otherwise
     * 
     * @see UapGenerator#initiateMediaSocket()
     */
    public boolean connectMediaSocket(InetAddress serverMediaAddress,
                                      int serverMediaPort)
    {
        if(_mediaSocket == null || _mediaSocket.isBound() == false)
        {
            logger.error("Trying to connect uninitialized socket");
            return false;
        }
        
        if(_mediaSocket.isConnected())
        {
            logger.verbose("Media socket already connected");
            return true;
        }
           
            
        try
        {
            SocketAddress mediaServerAddress = 
                    new InetSocketAddress(serverMediaAddress, serverMediaPort);
            _mediaSocket.connect(mediaServerAddress, (int) UapConstants.WAIT_FOR_MEDIA_BEARER_CFM_TIMEOUT);
            
        }
        catch (SocketTimeoutException e) {
            logger.debugObject("Media socket not connected within time[ms]:",
                    UapConstants.WAIT_FOR_MEDIA_BEARER_CFM_TIMEOUT);
            logger.errorException(e);
            return false;
        }
        catch (IOException e)
        {
            logger.errorException(e);
            return false;
        }
        
        logger.debugObject("Media socket connected generatorId=0x",
                Integer.toHexString(_generatorId));
        
        return true;
    }
    
    /**
     * Retrieves InputStream from media socket
     * @return stream for reading bytes from associated media socket or
     * <code>null</code> when stream cannot be obtained
     */
    public InputStream getMediaStream()
    {
        if(_mediaSocket != null && _mediaSocket.isConnected())
        {
            try
            {
                return _mediaSocket.getInputStream();
            } catch (IOException e)
            {
                logger.errorException(e);
            }
        }
        return null;
    }
}
